본문으로 건너뛰기

Redux vs Recoil vs Zustand

작성일 : 2025.01.19

React 생태계에서는 다양한 상태관리 라이브러리들이 경쟁하고 있는데, 가장 많이 사용되는 Redux, Recoil, Zustand를 비교해보려고 합니다.

Redux: 견고한 아키텍처의 대명사​

Redux는 React 생태계에서 가장 오랫동안 사랑받아온 상태 관리 라이브러리입니다. 예측 가능한 상태 흐름과 강력한 개발자 도구를 제공하는 것이 특징입니다.

Redux의 주요 특징​

  1. 중앙 집중식 스토어

    • 단일 스토어에서 모든 상태 관리
    • 예측 가능한 단방향 데이터 흐름
    • 시간 여행 디버깅 가능
  2. 강력한 미들웨어 시스템

    • redux-saga, redux-thunk 등 다양한 미들웨어
    • 비동기 작업 처리를 위한 풍부한 생태계
    • 로깅, 라우팅 등 다양한 미들웨어 지원
  3. DevTools 지원

    • 강력한 개발자 도구 제공
    • 상태 변화 추적 용이
    • 디버깅이 쉬움
  4. 큰 커뮤니티

    • 풍부한 레퍼런스와 문서
    • 안정적인 생태계
    • 많은 기업에서 사용중

기본 사용 예시​

// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

비동기 작업 예시​

// userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId) => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  }
);

const userSlice = createSlice({
  name: 'user',
  initialState: {
    entities: {},
    loading: 'idle',
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.loading = 'pending';
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.loading = 'idle';
        state.entities[action.payload.id] = action.payload;
      })
      .addCase(fetchUserById.rejected, (state, action) => {
        state.loading = 'idle';
        state.error = action.error;
      });
  },
});

실제 애플리케이션 예시​

// store.js
import { configureStore } from '@reduxjs/toolkit';
import logger from 'redux-logger';
import userReducer from './userSlice';
import cartReducer from './cartSlice';

export const store = configureStore({
  reducer: {
    user: userReducer,
    cart: cartReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(logger),
});

// CartComponent.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addItem, removeItem, updateQuantity } from './cartSlice';

function CartComponent() {
  const dispatch = useDispatch();
  const { items, total } = useSelector((state) => state.cart);
  const { user } = useSelector((state) => state.user);

  return (
    <div>
      <h2>Welcome, {user.name}</h2>
      <div className="cart-items">
        {items.map((item) => (
          <div key={item.id} className="cart-item">
            <span>{item.name}</span>
            <span>${item.price}</span>
            <input
              type="number"
              value={item.quantity}
              onChange={(e) =>
                dispatch(updateQuantity({
                  id: item.id,
                  quantity: parseInt(e.target.value)
                }))
              }
            />
            <button onClick={() => dispatch(removeItem(item.id))}>
              Remove
            </button>
          </div>
        ))}
      </div>
      <div className="cart-total">
        Total: ${total}
      </div>
    </div>
  );
}

Redux vs 다른 라이브러리들​

  1. vs Recoil

    • Redux는 더 체계적인 상태 관리
    • 더 큰 커뮤니티와 생태계
    • 더 많은 보일러플레이트 코드
  2. vs Zustand

    • Redux는 더 엄격한 패턴 제공
    • 더 강력한 개발자 도구
    • 더 큰 번들 사이즈
  3. vs Jotai

    • Redux는 중앙 집중식 상태 관리
    • 더 복잡한 설정 필요
    • 더 체계적인 상태 관리

언제 Redux를 사용하면 좋을까?​

  1. 대규모 애플리케이션에서 체계적인 상태 관리가 필요할 때
  2. 복잡한 상태 로직과 비동기 작업이 많을 때
  3. 팀 규모가 크고 일관된 코드 패턴이 필요할 때
  4. 엄격한 아키텍처가 요구되는 프로젝트
  5. 디버깅과 상태 추적이 중요한 경우

Redux는 확실한 패턴과 규칙을 제공하여 큰 규모의 프로젝트에서 안정적인 상태 관리를 가능하게 합니다. 특히 많은 개발자가 참여하는 프로젝트에서 그 진가를 발휘하며, 강력한 개발자 도구와 미들웨어 생태계는 복잡한 애플리케이션 개발에 큰 도움이 됩니다.

Recoil: React를 위한 새로운 패러다임​

Recoil은 Facebook에서 개발한 상태 관리 라이브러리로, React의 철학과 가장 잘 어울리는 접근 방식을 제공합니다. atom과 selector를 통한 상태 관리가 특징입니다.

Recoil의 주요 특징​

  1. Atom 기반 상태 관리

    • 가장 작은 상태 단위인 atom
    • 컴포넌트 간 상태 공유 용이
    • 상태의 모듈화가 쉬움
  2. Selector를 통한 파생 상태

    • 상태 간의 의존성 관리
    • 비동기 데이터 처리 용이
    • 계산된 값의 캐싱 지원
  3. React 친화적 설계

    • React Concurrent Mode 지원
    • Suspense 통합
    • Code Splitting 최적화
  4. 동시성 모드 지원

    • 비동기 상태 처리에 최적화
    • 비동기 셀렉터 지원
    • 상태 충돌 방지

기본 사용 예시​

import { atom, useRecoilState, selector, useRecoilValue } from 'recoil';

// 기본 atom 정의
const counterState = atom({
  key: 'counterState',
  default: 0,
});

// selector를 통한 파생 상태
const doubledState = selector({
  key: 'doubledState',
  get: ({get}) => {
    const count = get(counterState);
    return count * 2;
  },
});

function Counter() {
  const [count, setCount] = useRecoilState(counterState);
  const doubledCount = useRecoilValue(doubledState);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubledCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

비동기 데이터 처리 예시​

import { selector, selectorFamily, useRecoilValue } from 'recoil';

const userQuery = selectorFamily({
  key: 'UserQuery',
  get: (userId) => async () => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
});

const userStatsQuery = selector({
  key: 'UserStatsQuery',
  get: async ({get}) => {
    const response = await fetch('/api/user-stats');
    return response.json();
  },
});

function UserProfile({userId}) {
  const user = useRecoilValue(userQuery(userId));
  const stats = useRecoilValue(userStatsQuery);

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Posts: {stats.totalPosts}</p>
      <p>Following: {stats.following}</p>
    </div>
  );
}

실제 애플리케이션 예시​

import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';

// 상태 정의
const todoListState = atom({
  key: 'todoListState',
  default: [],
});

const todoListFilterState = atom({
  key: 'todoListFilterState',
  default: 'Show All',
});

const filteredTodoListState = selector({
  key: 'filteredTodoListState',
  get: ({get}) => {
    const list = get(todoListState);
    const filter = get(todoListFilterState);

    switch (filter) {
      case 'Show Completed':
        return list.filter((item) => item.isComplete);
      case 'Show Uncompleted':
        return list.filter((item) => !item.isComplete);
      default:
        return list;
    }
  },
});

function TodoList() {
  const [todoList, setTodoList] = useRecoilState(todoListState);
  const [filter, setFilter] = useRecoilState(todoListFilterState);
  const filteredList = useRecoilValue(filteredTodoListState);

  const addItem = (text) => {
    setTodoList((oldList) => [
      ...oldList,
      {
        id: getId(),
        text,
        isComplete: false,
      },
    ]);
  };

  return (
    <div>
      <TodoListStats />
      <TodoListFilters 
        filter={filter} 
        setFilter={setFilter}
      />
      <TodoItemCreator addItem={addItem} />
      {filteredList.map((item) => (
        <TodoItem key={item.id} item={item} />
      ))}
    </div>
  );
}

Recoil vs 다른 라이브러리들​

  1. vs Redux

    • 더 적은 보일러플레이트
    • React에 더 최적화된 API
    • 비동기 처리가 더 간단
  2. vs Zustand

    • 더 세밀한 상태 관리
    • Suspense 지원
    • React 생태계와 더 긴밀한 통합
  3. vs Jotai

    • 더 많은 기능 제공
    • Facebook의 공식 지원
    • 더 큰 번들 사이즈

언제 Recoil을 사용하면 좋을까?​

  1. React의 최신 기능을 최대한 활용하고 싶을 때
  2. 비동기 상태 관리가 많은 프로젝트
  3. 상태 간 의존성이 복잡한 경우
  4. Code Splitting이 중요한 프로젝트
  5. Concurrent Mode를 활용하고 싶은 경우

Recoil은 React의 철학을 깊이 이해하고 이를 상태 관리에 반영한 현대적인 라이브러리입니다. 특히 비동기 데이터 처리와 상태 의존성 관리가 중요한 프로젝트에서 큰 강점을 발휘하며, React의 최신 기능들과 잘 통합되어 있습니다.

Zustand: 심플함의 미학​

Zustand는 미니멀리즘을 추구하는 상태 관리 라이브러리입니다. "단순하고 확장 가능한" 이라는 모토처럼, 최소한의 코드로 강력한 상태 관리 기능을 제공합니다.

Zustand의 주요 특징​

  1. 단순한 설정

    • Provider 없이 사용 가능
    • 최소한의 보일러플레이트
    • 직관적인 API
  2. 강력한 성능

    • 작은 번들 사이즈 (~1KB)
    • 불필요한 리렌더링 방지
    • 메모리 효율적인 구현
  3. 유연한 확장성

    • 미들웨어 시스템 지원
    • DevTools 통합
    • TypeScript 지원
  4. 편리한 상태 관리

    • 훅 기반의 API
    • 자동 상태 병합
    • 선택적 상태 구독

기본 사용 예시​

import create from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

function Counter() {
  const { count, increment, decrement, reset } = useStore();

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

미들웨어 사용 예시​

import create from 'zustand';
import { persist, devtools } from 'zustand/middleware';

const useStore = create(
  devtools(
    persist(
      (set) => ({
        theme: 'light',
        toggleTheme: () =>
          set((state) => ({
            theme: state.theme === 'light' ? 'dark' : 'light',
          })),
      }),
      {
        name: 'theme-storage',
        getStorage: () => localStorage,
      }
    )
  )
);

실제 애플리케이션 예시​

import create from 'zustand';
import { devtools } from 'zustand/middleware';

interface User {
  id: string;
  name: string;
  email: string;
}

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface AppState {
  // 사용자 상태
  user: User | null;
  setUser: (user: User | null) => void;
  
  // 장바구니 상태
  cart: CartItem[];
  addToCart: (item: Omit<CartItem, 'quantity'>) => void;
  removeFromCart: (itemId: string) => void;
  updateQuantity: (itemId: string, quantity: number) => void;
  
  // 계산된 값
  cartTotal: () => number;
}

const useStore = create<AppState>()(
  devtools((set, get) => ({
    // 사용자 상태 관리
    user: null,
    setUser: (user) => set({ user }),
    
    // 장바구니 상태 관리
    cart: [],
    addToCart: (item) => 
      set((state) => {
        const existingItem = state.cart.find((i) => i.id === item.id);
        if (existingItem) {
          return {
            cart: state.cart.map((i) =>
              i.id === item.id
                ? { ...i, quantity: i.quantity + 1 }
                : i
            ),
          };
        }
        return {
          cart: [...state.cart, { ...item, quantity: 1 }],
        };
      }),
    
    removeFromCart: (itemId) =>
      set((state) => ({
        cart: state.cart.filter((item) => item.id !== itemId),
      })),
    
    updateQuantity: (itemId, quantity) =>
      set((state) => ({
        cart: state.cart.map((item) =>
          item.id === itemId
            ? { ...item, quantity }
            : item
        ),
      })),
    
    // 계산된 값
    cartTotal: () => {
      const cart = get().cart;
      return cart.reduce(
        (total, item) => total + item.price * item.quantity,
        0
      );
    },
  }))
);

// 사용 예시
function ShoppingCart() {
  const { cart, cartTotal, removeFromCart, updateQuantity } = useStore();

  return (
    <div>
      {cart.map((item) => (
        <div key={item.id}>
          <span>{item.name}</span>
          <input
            type="number"
            value={item.quantity}
            onChange={(e) => 
              updateQuantity(item.id, parseInt(e.target.value, 10))
            }
          />
          <button onClick={() => removeFromCart(item.id)}>
            Remove
          </button>
        </div>
      ))}
      <div>Total: ${cartTotal()}</div>
    </div>
  );
}

Zustand vs 다른 라이브러리들​

  1. vs Redux

    • 더 적은 보일러플레이트
    • 더 간단한 설정
    • 더 작은 번들 사이즈
  2. vs Recoil

    • Provider 불필요
    • 더 단순한 API
    • 더 빠른 학습 곡선
  3. vs Jotai

    • 중앙 집중식 상태 관리
    • 더 명확한 상태 구조
    • 더 간단한 API

언제 Zustand를 사용하면 좋을까?​

  1. 빠른 개발과 간단한 설정이 필요할 때
  2. 작은 규모에서 중간 규모의 프로젝트
  3. 성능과 번들 사이즈가 중요한 경우
  4. Redux의 보일러플레이트를 피하고 싶을 때
  5. TypeScript와 함께 사용하고 싶은 경우

Zustand는 단순함과 유연성을 동시에 제공하는 현대적인 상태 관리 라이브러리입니다. 특히 작은 프로젝트부터 중간 규모의 프로젝트까지 빠르게 개발하면서도 안정적인 상태 관리가 필요한 경우에 탁월한 선택이 될 수 있습니다.

Jotai: React를 위한 원시적이고 유연한 상태 관리 라이브러리​

Jotai는 Recoil에서 영감을 받았지만, 더 단순하고 원시적인 접근 방식을 취하는 상태 관리 라이브러리입니다. "primitive and flexible state management for React"라는 슬로건처럼, 매우 미니멀하면서도 강력한 기능을 제공합니다.

Jotai의 주요 특징​

  1. Bottom-up 접근방식

    • 작은 단위(atom)에서 시작하여 상태를 구성
    • 모듈러하고 재사용 가능한 상태 관리 가능
  2. TypeScript 우선 설계

    • 처음부터 TypeScript를 고려하여 설계됨
    • 뛰어난 타입 추론과 타입 안정성
  3. 작은 번들 사이즈

    • 기본 번들 크기가 약 2.4kB로 매우 작음
  4. Provider 선택적 사용

    • Provider 없이도 사용 가능
    • 필요한 경우에만 Provider 사용

기본 사용 예시​

import { atom, useAtom } from 'jotai'

// 기본 atom 생성
const countAtom = atom(0)
const doubleAtom = atom((get) => get(countAtom) * 2)

// 읽기/쓰기 가능한 파생 atom
const countryAtom = atom('Japan')
const citiesAtom = atom(
  (get) => getCitiesOf(get(countryAtom)),
  (get, set, newCity) => {
    const country = get(countryAtom)
    if (!getCitiesOf(country).includes(newCity)) {
      set(countryAtom, findCountryOf(newCity))
    }
  }
)

// 컴포넌트에서 사용
function Counter() {
  const [count, setCount] = useAtom(countAtom)
  const [double] = useAtom(doubleAtom)

  return (
    <div>
      Count: {count}
      Double: {double}
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  )
}

고급 사용 예시​

import { atom, useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { focusAtom } from 'jotai/optics'

// localStorage와 연동
const themeAtom = atomWithStorage('theme', 'light')

// 비동기 atom
const userAtom = atom(async (get) => {
  const response = await fetch('/api/user')
  return response.json()
})

// 중첩된 객체의 특정 필드에 대한 atom
const formAtom = atom({
  name: '',
  age: 0,
  address: {
    street: '',
    city: '',
  }
})

// Optics를 사용한 중첩 객체 업데이트
const streetAtom = focusAtom(formAtom, (optic) => optic.prop('address').prop('street'))

function UserProfile() {
  const [user] = useAtom(userAtom)
  const [theme, setTheme] = useAtom(themeAtom)
  const [street, setStreet] = useAtom(streetAtom)

  return (
    <div>
      <div>User: {user.name}</div>
      <div>Theme: {theme}</div>
      <input 
        value={street} 
        onChange={(e) => setStreet(e.target.value)} 
      />
    </div>
  )
}

실제 애플리케이션 예시​

import { atom, useAtom } from 'jotai'
import { atomWithQuery } from 'jotai/query'

// 쇼핑몰 장바구니 구현
interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
}

const cartAtom = atom<CartItem[]>([])
const cartTotalAtom = atom((get) => {
  const cart = get(cartAtom)
  return cart.reduce((total, item) => total + item.price * item.quantity, 0)
})

// React Query와 통합
const productsAtom = atomWithQuery((get) => ({
  queryKey: ['products'],
  queryFn: async () => {
    const response = await fetch('/api/products')
    return response.json()
  },
}))

function ShoppingCart() {
  const [cart, setCart] = useAtom(cartAtom)
  const [total] = useAtom(cartTotalAtom)
  const [products] = useAtom(productsAtom)

  const addToCart = (product) => {
    setCart(prev => {
      const existing = prev.find(item => item.id === product.id)
      if (existing) {
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        )
      }
      return [...prev, { ...product, quantity: 1 }]
    })
  }

  return (
    <div>
      <h2>Products</h2>
      {products.map(product => (
        <div key={product.id}>
          <span>{product.name}</span>
          <button onClick={() => addToCart(product)}>Add to Cart</button>
        </div>
      ))}
      
      <h2>Cart</h2>
      {cart.map(item => (
        <div key={item.id}>
          {item.name} x {item.quantity}
        </div>
      ))}
      <div>Total: ${total}</div>
    </div>
  )
}

Jotai vs 다른 라이브러리들​

  1. vs Recoil

    • Jotai는 더 작고 단순한 API를 제공
    • Provider가 선택적
    • 더 작은 번들 사이즈
  2. vs Zustand

    • Jotai는 더 React스러운 접근방식
    • atom 기반의 세분화된 상태 관리
    • 더 나은 코드 분할 지원
  3. vs Redux

    • 훨씬 적은 보일러플레이트
    • 더 유연한 상태 구성
    • 더 나은 성능 (필요한 부분만 리렌더링)

언제 Jotai를 사용하면 좋을까?​

  1. React와 긴밀하게 통합된 상태 관리가 필요할 때
  2. 작은 단위의 상태를 효율적으로 관리하고 싶을 때
  3. TypeScript 프로젝트에서 타입 안정성이 중요할 때
  4. 성능 최적화가 중요한 프로젝트에서
  5. 코드 분할이 필요한 대규모 애플리케이션에서

Jotai는 React의 기본 원칙을 따르면서도 강력한 상태 관리 기능을 제공하는 현대적인 라이브러리입니다. 특히 TypeScript 프로젝트에서 그 진가를 발휘하며, 작은 번들 사이즈와 뛰어난 성능으로 많은 개발자들의 선택을 받고 있습니다.

어떤 라이브러리를 선택해야 할까?​

각 라이브러리는 저마다의 특징이 있어 상황에 따라 선택이 달라질 수 있습니다.

어떤 라이브러리를 선택해야 할까?​

각 라이브러리는 저마다의 특징이 있어 상황에 따라 선택이 달라질 수 있습니다.

Redux가 좋은 경우​

  • 대규모 팀에서 일관된 상태 관리가 필요할 때
  • 복잡한 상태 로직과 미들웨어가 필요할 때
  • 엄격한 아키텍처가 요구될 때
  • 상태 변화 추적과 디버깅이 중요한 경우

Recoil이 좋은 경우​

  • React의 최신 기능을 최대한 활용하고 싶을 때
  • 비동기 상태 관리가 많은 프로젝트
  • 상태 간 의존성이 복잡한 경우
  • Concurrent Mode와 Suspense 활용이 필요할 때

Zustand가 좋은 경우​

  • 빠른 개발과 간단한 설정이 필요할 때
  • 작은 규모의 프로젝트
  • 성능과 번들 사이즈가 중요한 경우
  • Redux의 보일러플레이트를 피하고 싶을 때

Jotai가 좋은 경우​

  • React와 긴밀하게 통합된 상태 관리가 필요할 때
  • TypeScript 프로젝트에서 타입 안정성이 중요할 때
  • 작은 단위의 상태를 효율적으로 관리하고 싶을 때
  • 코드 분할이 필요한 대규모 애플리케이션에서
  • Bottom-up 방식의 상태 설계가 필요한 경우

결론​

상태관리 라이브러리 선택은 프로젝트의 규모, 팀의 경험, 요구사항 등을 종합적으로 고려해야 합니다. 각 라이브러리는 저마다의 강점이 있습니다:

  • Redux의 견고함: 체계적인 아키텍처와 풍부한 생태계로 대규모 프로젝트에 안정성 제공
  • Recoil의 혁신성: React의 최신 기능들과 완벽한 통합으로 현대적인 상태 관리 가능
  • Zustand의 단순함: 미니멀한 API와 빠른 학습 곡선으로 효율적인 개발 지원
  • Jotai의 유연성: TypeScript 친화적이고 세분화된 상태 관리로 정교한 제어 가능

중요한 것은 "어떤 도구가 가장 좋은가?" 가 아니라 "우리 프로젝트에 어떤 도구가 가장 적합한가?" 입니다. 상태관리 라이브러리는 프론트엔드 개발의 중요한 도구이지만, 궁극적으로는 도구일 뿐입니다.

현명한 개발자는 각 도구의 장단점을 이해하고, 프로젝트의 요구사항, 팀의 역량, 유지보수 용이성 등을 종합적으로 고려하여 최적의 선택을 할 수 있어야 합니다. 때로는 여러 라이브러리를 함께 사용하거나, 상황에 따라 다른 선택을 하는 유연한 접근도 필요할 수 있습니다.